【SFA官方翻译】:使用 Spring Boot 2.0、Eureka 和 Spring Cloud 的微服务快速指南
原文链接:https://dzone.com/articles/quick-guide-to-microservices-with-spring-boot-20-e
作者:Piotr Mińkowski
译者:Darren Luo
本文提供 Spring Boot 和 Spring Cloud 用于处理微服务的最重要组件的小结
我的博客上有许多关于使用 Spring Boot 和 Spring Cloud 的微服务的文章。本文的主要目的是简要概述这些框架提供的能帮助你创建微服务的重要组件。本文的主题涉及:
在云原生开发中使用 Spring Boot 2.0
使用 Spring Cloud Netflix Eureka 为所有微服务提供服务发现
使用 Spring Cloud Config 分发配置
使用 Spring Cloud 中的新项目:Spring Cloud Gateway 进行 API 网关匹配
使用 Spring Cloud Sleuth 进行日志关联
在我们处理源码前,让我们看一下下图。它说明了我们的示例系统的架构。我们有三个独立的微服务,它们在服务发现中注册它们自己,从配置服务中获取属性,并互相通信。整个系统隐藏在 API 网关后面。
目前,Spring Cloud 的最新版本是 Finchley.M9
。该版本的 spring-cloud-dependencies
应该为依赖管理声明为 BOM。
<?xml version="1.0" encoding="UTF-8"?>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.M9</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
现在,让我们考虑进一步措施,以便使用 Spring Cloud 创建一个可用的基于微服务的系统。我们将从 配置服务器 开始。
本文中的示例程序的源码可在 GitHub repository 中找到。
步骤1. 使用 Spring Cloud Config 构建配置服务器
要为应用程序启用 Spring Cloud Config 功能,首先在你的项目依赖中引入 spring-cloud-config-server
。
<?xml version="1.0" encoding="UTF-8"?>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
然后用 @EnableConfigServer
注解在应用启动期间运行一个嵌入式配置服务。
@SpringBootApplication
@EnableConfigServer
public class ConfigApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ConfigApplication.class).run(args);
}
}
默认情况下,Spring Cloud Config 服务在 Git repository 中存储配置数据。这在生产模式中是一个非常好的选择,但是对于这个示例,文件系统后端就够用了。从配置服务开始真的非常容易,因为我们能在 classpath 中放置所有属性。Spring Cloud Config 默认在以下位置搜索属性源: classpath:/
、 classpath:/config
、 file:./
、 file:./config
。
我们将所有属性源放到 src/main/resources/config
里。YAML 文件名将与服务名相同。例如,服务发现的 YAML 文件将被放在这里: src/main/resources/config/discovery-service.yml
。
两个最重要的事。如果你想使用文件系统后端启用配置服务,你必须激活本地 Spring Boot 配置。这可以在应用程序启动时通过设置参数 --spring.profiles.active=native
实现。我也可以通过设置 bootstrap.yml
文件中的 server.port
属性修改默认配置服务端口(8888)为 8061。
步骤2. 使用 Spring Cloud Netflix Eureka 构建服务发现
最重要的是配置服务。现在,所有其他应用程序,包括服务发现,都需要添加 spring-cloud-starter-config
依赖以启用配置客户端。我们还必须添加 spring-cloud-starter-netflix-eureka-server
依赖。
<?xml version="1.0" encoding="UTF-8"?>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
然后,你应该通过在主类上设置 @EnableEurekaServer
注解在应用程序启动时启用嵌入式发现服务。
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(DiscoveryApplication.class).run(args);
}
}
应用程序必须从配置服务器获取属性源。客户端需要的最小化配置是应用程序名和配置服务器的连接设置。
spring:
application:
name: discovery-service
cloud:
config:
uri: http://localhost:8088
正如我已经提到的,该配置文件 discovery-service.yml
应该放置在 discovery-service.yml
模块里。然而,我必须就下面可见的配置说一下。我们已经将 Eureka 运行端口从默认值(8761)修改为 8061。对于一个独立 Eureka 实例,我们必须禁用注册和获取注册表。
server:
port: 8061
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
现在,当你使用嵌入式 Eureka 服务器启动你的应用程序,你应该会看到以下日志。
一旦你成功启动应用程序,你可以在地址 http://localhost:8061/ 查看 Eureka 仪表盘。
步骤3. 使用 Spring Boot 和 Spring Cloud 构建微服务
我们的为服务必须在启动时执行一些操作。它需要从 config-service
或许配置,在发现服务中注册其本身,公开 HTTP API,并自动生成 API 文档。要启用这些机制,我们需要在 pom.xml
中引入一些依赖。要启用配置客户端,我们因该引入 spring-cloud-starter-config
启动器。发现客户端将在引入 spring-cloud-starter-netflix-eureka-client
并用 @EnableDiscoveryClient
注解主类后为微服务启用。要强制 Spring Boot 应用程序生成 API 文档,我们需要引入 springfox-swagger2
依赖并添加 @EnableSwagger2
注解。
这是为我们的示例微服务定义的全部依赖列表:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.8.0</version>
</dependency>
下面是为为服务启用 Discovery Client 和 Swagger2 的应用程序的主类:
@SpringBootApplication
@EnableDiscoveryClient
@EnableSwagger2
public class EmployeeApplication {
public static void main(String[] args) {
SpringApplication.run(EmployeeApplication.class, args);
}
@Bean
public Docket swaggerApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("pl.piomin.services.employee.controller"))
.paths(PathSelectors.any())
.build()
.apiInfo(new ApiInfoBuilder().version("1.0").title("Employee API").description("Documentation Employee API v1.0").build());
}
...
}
应用程序必须从远程服务器获取配置,所以我们应该只提供一份带有服务名和服务器 URL 的 bootstrap.yml
文件。事实上,这是 Config First Bootstrap 理论的一个示例,即一个应用程序首先连接到配置服务器并从远程属性源获取发现服务的地址。还有一种 Discovery First Bootstrap,即从发现服务获取配置服务器地址。
spring:
application:
name: employee-service
cloud:
config:
uri: http://localhost:8088
配置设置可能不是很多。这是存储在远程服务器上的应用程序配置文件。它只保存了 HTTP 运行端口和 Eureka URL。然而,我还将文件 employee-service-instance2.yml
放在了远程配置服务器上。它为应用程序设置不同的 HTTP 端口,所以你可以基于远程属性在本地运行相同服务的两个实例。现在,你可以在应用程序启动期间传递参数 spring.profiles.active=instance2
后在端口 9090 上运行第二个 employee-service
实例。使用默认配置,你将在端口 8090 上启动微服务。
server:
port: 9090
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8061/eureka/
下面是 REST controller 类的代码实现。它提供了添加新员工和使用不同过滤器搜索员工的实现。
@RestController
public class EmployeeController {
private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeController.class);
@Autowired
EmployeeRepository repository;
@PostMapping
public Employee add(@RequestBody Employee employee) {
LOGGER.info("Employee add: {}", employee);
return repository.add(employee);
}
@GetMapping("/{id}")
public Employee findById(@PathVariable("id") Long id) {
LOGGER.info("Employee find: id={}", id);
return repository.findById(id);
}
@GetMapping
public List findAll() {
LOGGER.info("Employee find");
return repository.findAll();
}
@GetMapping("/department/{departmentId}")
public List findByDepartment(@PathVariable("departmentId") Long departmentId) {
LOGGER.info("Employee find: departmentId={}", departmentId);
return repository.findByDepartment(departmentId);
}
@GetMapping("/organization/{organizationId}")
public List findByOrganization(@PathVariable("organizationId") Long organizationId) {
LOGGER.info("Employee find: organizationId={}", organizationId);
return repository.findByOrganization(organizationId);
}
}
步骤4. 使用 Spring Cloud Open Feign 在微服务间通信
我们得第一个微服务已经创建并启动。现在,我们将添加其他相互通信的微服务。下图说明了三个示例微服务之间的通信流: organization-service
、 department-service
和 employee-service
。微服务 organization-service
从 department-service
收集带有员工( GET/organization/{organizationId}/with-employees
)或不带员工( GET/organization/{organizationId}
)的部门列表,直接从 employee-service
获取未将他们划分到不同部门的员工列表。微服务 department-service
可以收集分配给特定部门的员工列表。
如上方案所述, organization-service
和 department-service
都必须定位到其他微服务并和他们进行通信。这就是为什么我们需要为这些模块引入一个额外的依赖:spring-cloud-starter-openfeign。Spring Cloud Open Feign 是一个使用 Ribbon 与其他微服务通信进行客户端负载均衡的声明式 REST 客户端。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Open Feign 的替代方案是使用 @LoadBalanced
的 Spring RestTemplate
。但是,Feign 提供了一种更优雅的方式定义客户端,所以我更喜欢用它替代 RestTemplate
。再引入所需的依赖后,我们还应该使用 @EnableFeignClients
注解启用 Feign 客户端。
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableSwagger2
public class OrganizationApplication {
public static void main(String[] args) {
SpringApplication.run(OrganizationApplication.class, args);
}
...
}
现在,我们需要定义客户端的接口。因为 organization-service
和其他两个微服务通信,我们需要创建两个接口,每个微服务一个。每个客户端接口应该用 @FeignClient
注解。注解中 name
字段是必须的。name 必须注册在服务发现中的目标服务名相同。下面是调用由 employee-service
公开的 GET/organization/{organizationId}
endpoint 的客户端接口。
@FeignClient(name = "employee-service")
public interface EmployeeClient {
@GetMapping("/organization/{organizationId}")
List findByOrganization(@PathVariable("organizationId") Long organizationId);
}
organization-service
里可用的第二个客户端接口从 department-service
调用了两个 endpoint。其中第一个, GET/organization/{organizationId}
,仅返回组织的可用部门列表,而第二个, GET/organization/{organizationId}/with-employees
,返回相同的数据集,包含分配给每个部门的员工列表。
@FeignClient(name = "department-service")
public interface DepartmentClient {
@GetMapping("/organization/{organizationId}")
public List findByOrganization(@PathVariable("organizationId") Long organizationId);
@GetMapping("/organization/{organizationId}/with-employees")
public List findByOrganizationWithEmployees(@PathVariable("organizationId") Long organizationId);
}
最后,我们必须将 Feign 客户端 bean 注入到 REST controller。现在,我们可以调用 DepartmentClient
和 EmployeeClient
中定义的方法了,这等同于调用 REST endpoint。
@RestController
public class OrganizationController {
private static final Logger LOGGER = LoggerFactory.getLogger(OrganizationController.class);
@Autowired
OrganizationRepository repository;
@Autowired
DepartmentClient departmentClient;
@Autowired
EmployeeClient employeeClient;
...
@GetMapping("/{id}")
public Organization findById(@PathVariable("id") Long id) {
LOGGER.info("Organization find: id={}", id);
return repository.findById(id);
}
@GetMapping("/{id}/with-departments")
public Organization findByIdWithDepartments(@PathVariable("id") Long id) {
LOGGER.info("Organization find: id={}", id);
Organization organization = repository.findById(id);
organization.setDepartments(departmentClient.findByOrganization(organization.getId()));
return organization;
}
@GetMapping("/{id}/with-departments-and-employees")
public Organization findByIdWithDepartmentsAndEmployees(@PathVariable("id") Long id) {
LOGGER.info("Organization find: id={}", id);
Organization organization = repository.findById(id);
organization.setDepartments(departmentClient.findByOrganizationWithEmployees(organization.getId()));
return organization;
}
@GetMapping("/{id}/with-employees")
public Organization findByIdWithEmployees(@PathVariable("id") Long id) {
LOGGER.info("Organization find: id={}", id);
Organization organization = repository.findById(id);
organization.setEmployees(employeeClient.findByOrganization(organization.getId()));
return organization;
}
}
步骤5. 使用 Spring Cloud Gateway 构建 API 网关
Spring Cloud Gateway 是相对较新的 Spring Cloud 项目。它构建于 Spring Framework 5、Project Reactor 和 Spring Boot 2.0 之上。它需要由 Spring Boot 和 Spring Webflux 提供的 Netty 运行环境。这是 Spring Cloud Netflix Zuul 的很好的替代品,它是迄今为止唯一微服务提供 API 网关的 Spring Cloud 项目。
API 网关实现在 gateway-service
模块内。首先,我们应该引入 spring-cloud-starter-gateway
启动器到项目依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
我们还需要启用发现客户端,因为 gateway-service
与 Eureka 集成可以启用执行到下游服务的路由。网关还将公开通过我们实例为夫妇公开的所有 endpoint 的 API 规范。这就是我们在网关上启用 Swagger2 的原因。
@SpringBootApplication
@EnableDiscoveryClient
@EnableSwagger2
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
Spring Cloud Gateway 提供三种用于配置的基础组件:route、predicate 和 filter。route *是网关的基本构建块。它包含目标 URI 和已定义的 predicate 和 filter 的列表。predicate* 负责对传入的 HTTP 请求中的任何东西进行匹配,比如请求头或参数。filter 可以在发送到下游服务前后修改请求或响应。所有这些组件都可以用配置属性进行设置。我们将创建并将起放置在配置服务器的 gateway-service.yml 文件上,并未我们的示例微服务定义route。
但是首先,我们通过设置属性 spring.cloud.gateway.discovery.locator.enabled
为 true 来启用和发现服务的集成。然后我们可以继续定义 route 规则。我们使用 Path Route Predicate Factory 匹配的入站请求,使用 RewritePath GatewayFilter Factory 修改请求路径使其适应由下游服务公开的格式。URI 参数指定在发现服务注册的目标服务名。让我们看看以下路由定义。例如,为了使 organization-service
在路由上的 /organization/**
路径下可用,我们应该定义 predicate 为 Path=/organization/**
,然后从路径中去除前缀 /organization
,因为目标服务公开在路径 /**
下。基于 URI 值为 Eureka 获取的目标服务的地址为 lb://organization-service
。
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: employee-service
uri: lb://employee-service
predicates:
- Path=/employee/**
filters:
- RewritePath=/employee/(?<path>.*), /$\{path}
- id: department-service
uri: lb://department-service
predicates:
- Path=/department/**
filters:
- RewritePath=/department/(?<path>.*), /$\{path}
- id: organization-service
uri: lb://organization-service
predicates:
- Path=/organization/**
filters:
- RewritePath=/organization/(?<path>.*), /$\{path}
步骤6. 使用 Swagger2 在网关上启用 API 规范
每个使用 @EnableSwagger2
注解的 Spring Boot 微服务都在 /v2/api-docs
路径下公开 Swagger API 文档。但是,我们希望将文档放在一个位置,在 API 网关上。要实现它,我们需要在 gateway-service
模块里提供一个实现了 SwaggerResourcesProvider
接口的 bean。这个 bean 负责定义应该由应用程序显示的 Swagger 资源的存储列表。以下是基于 Spring Cloud Gateway 配置属性的从服务发现获取到必要地址的 SwaggerResourcesProvider
的实现。
不幸的是,SpringFox Swagger 仍然不支持 Spring WebFlux。这意味着如果你在项目中引入 SpringFox Swagger 依赖,应用程序将启动失败。我希望很快就会有 WebFlux 的支持,但是现在我们必须使用 Spring Cloud Netflix Zuul 作为网关,如果我们在其上运行嵌入式 Swagger2。
我创建了基于 Netflix Zuul 的到 gateway-service
替代基于 Spring Cloud Gateway API 网关的 proxy-service
模块。这是 proxy-service
内的可用的 SwaggerResourcesProvider 实现 bean。它使用 ZuulProperties
bean 动态的将路由定义加载到 bean 中。
@Configuration
public class ProxyApi {
@Autowired
ZuulProperties properties;
@Primary
@Bean
public SwaggerResourcesProvider swaggerResourcesProvider() {
return () -> {
List resources = new ArrayList();
properties.getRoutes().values().stream()
.forEach(route -> resources.add(createResource(route.getServiceId(), route.getId(), "2.0")));
return resources;
};
}
private SwaggerResource createResource(String name, String location, String version) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation("/" + location + "/v2/api-docs");
swaggerResource.setSwaggerVersion(version);
return swaggerResource;
}
}
这是我们的示例微服务系统的 Swagger UI,位于 http://localhost:8060/swagger-ui.html。
步骤7. 运行应用程序
我们来看看下图可见的我们系统的架构。我们将从视图的 organization-service
开始讨论它。在启动后, organization-service
连接到地址 localhost:8088(1)下可用的 config-service
。基于远程配置设置,它可以在 Eureka(2)注册它自己。当 organization-service
的 endpoint 被外部客户端通过地址 localhost:8060 下可用的网关(3)执行,请求被转发到基于来自服务发现(4)的记录的 organization-service
实例。然后 organization-service
在 Eureka(5)中查找 department-service
的地址,并调用它的 endpoint(6)。最后, department-service
从 employee-service
调用 endpoint。请求通过 Ribbon(7) 在两个 employee-service
可用实例进行负载均衡。
我们来看看 http://localhost:8061/ 下的 Eureka 仪表盘。那里注册了四个微服务实例:单独的 organization-service
和 department-service
实例,两个 department-service
实例。
现在,让我们调用 endpoint http://localhost:8060/organization/1/with-departments-and-employees。
步骤8. 使用 Spring Cloud Sleuth 聚合独立微服务间的日志
使用 Spring Cloud Sleuth 聚合不同微服务间的日志非常简单。事实上,你唯一要做的就是将 spring-cloud-starter-sleuth
启动器添加到每个独立微服务和网关的依赖中。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
为了证明,我们将默认日志格式稍微修改为: %d{yyyy-MM-dd HH:mm:ss}${LOG_LEVEL_PATTERN:-%5p}%m%n
。下面是由我们三个示例微服务生成的日志。Spring Cloud Stream 生成的括号 []
内有四个条目。对我们来说最重要的是第二个条目,它表示在系统外围为每个传入的 HTTP 请求设置一次 traceId
。
招人:数心,造化心数奇;用心等你...
上一篇:【SFA官方翻译】在Java中应用微服务开发RESTful APIs
点击阅读原文查看更多